package cfdt.common.jwt;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import cfdt.common.utils.HMacUtil;
import cfdt.tools.json.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JWT Token对象
 * 源码来自于CFCA无纸化，重构了JWT依赖源码
 */
public class JwtToken {

    private static final Logger logger = LoggerFactory.getLogger(JwtToken.class);

    public static final char SEPARATOR_CHAR = '.';

    private JwtHeader header = new JwtHeader();
    private JwtClaims claims;

    private String hmac;
    private StringBuffer token = new StringBuffer();


    public static String EXPIRED_MESSAGE = "token is expired";
    public static String INVALID_MESSAGE = "token is invalid";

    /**
     * 构造JWT Token对象 通过业务值
     */
    public JwtToken(String algName, JwtClaims claimsData, String secret) {
        if (StrUtil.isEmpty(algName)) {
            throw new IllegalArgumentException("algName is null");
        }
        if (StrUtil.isEmpty(secret)) {
            throw new IllegalArgumentException("secret is null");
        }
        if (null == claimsData) {
            throw new IllegalArgumentException("claimsData is null");
        }
        if (claimsData.getParamsSize() == 0) {
            throw new IllegalArgumentException("claimsData is null");
        }

        // 设置头算法
        this.header.setAlgName(algName);
        // 设置业务参数
        this.claims = claimsData;
        // 构建业务参数源数据
        this.buildSourceData();
        // 计算HASH
        this.hmac = HMacUtil.sm3Base64(secret, this.token.toString());
        // 生成token
        this.token.append(SEPARATOR_CHAR).append(this.hmac);
    }

    /**
     * 构造JWT Token对象 通过token值
     */
    public JwtToken(String tokenString, String secretKey) {
        if (StrUtil.isEmpty(tokenString)) {
            throw new IllegalArgumentException("token is null");
        }
        if (StrUtil.isEmpty(secretKey)) {
            throw new IllegalArgumentException("secretKey is null");
        }
        // 解析token
        String base64UrlEncodedHeader = null;
        String base64UrlEncodedPayload = null;
        String base64UrlEncodedDigest = null;
        int delimiterCount = 0;
        StringBuilder sb = new StringBuilder(128);
        for(char c : tokenString.toCharArray()) {
            if (c == '.') {
                if (delimiterCount == 0) {
                    base64UrlEncodedHeader = sb.toString();
                    sb = new StringBuilder(128);
                } else if (delimiterCount == 1) {
                    base64UrlEncodedPayload = sb.toString();
                    sb = new StringBuilder(128);
                }
                ++delimiterCount;
            } else {
                sb.append(c);
            }
        }
        if (delimiterCount != 2) {
            String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount;
            throw new IllegalArgumentException(msg);
        }
        if (sb.length() > 0) {
            base64UrlEncodedDigest = sb.toString();
        }
        try {
            // 头信息解析
            String headerString = new String(Base64.decode(base64UrlEncodedHeader), StandardCharsets.UTF_8);
            HashMap<String, String> gsonHeader = JsonUtil.toBean(headerString, HashMap.class);
            String alg = gsonHeader.get(JwtHeader.ALGORITHM);
            this.header.setAlgName(alg);

            // 数据MAP信息解析
            String claimsString = new String(Base64.decode(base64UrlEncodedPayload), StandardCharsets.UTF_8);
            Map<String, Object> gsonClaims = JsonUtil.toBean(claimsString, HashMap.class);
            this.claims = new JwtClaims();
            Object originClaims = gsonClaims.get(JwtClaims.CLAIMS);
            Map<String, Object> originClaims2 = (Map)originClaims;
            this.claims.putAll(originClaims2);

            // 解析并校验签名
            this.buildSourceData();
            String realHmac = HMacUtil.sm3Base64(secretKey, this.token.toString());

            // 签名验证失败
            if (StrUtil.isBlank(base64UrlEncodedDigest)) {
                throw new IllegalArgumentException(INVALID_MESSAGE);
            }
            if (!base64UrlEncodedDigest.equals(realHmac)) {
                throw new IllegalArgumentException(INVALID_MESSAGE);
            }
        } catch (IllegalArgumentException e) {
            throw e;
        } catch (Exception e) {
            logger.error("parseToken failed", e);
            throw new IllegalArgumentException(e.getMessage(), e);
        }
    }

    /**
     * 构建JWT的原始数据部分（header和claims的Base64编码组合）
     *
     * 将header和claims分别序列化为JSON字符串，然后进行Base64编码，
     * 并用'.'分隔符连接，最后追加到token缓冲区中
     */
    private void buildSourceData() {
        String headerString = JsonUtil.toJsonStr(this.header);
        String claimsString = JsonUtil.toJsonStr(this.claims);
        String sourceString = Base64.encode(headerString.getBytes(StandardCharsets.UTF_8))
                + SEPARATOR_CHAR
                + Base64.encode(claimsString.getBytes(StandardCharsets.UTF_8));
        this.token.append(sourceString);
    }

    /**
     * 获取完整的JWT Token字符串
     *
     * @return 返回完整的JWT Token字符串，格式为：header.claims.signature
     */
    public String getToken() {
        return this.token.toString();
    }

    /**
     * 获取JWT的Header部分
     *
     * @return 返回JwtHeader对象
     */
    public JwtHeader getHeader() {
        return this.header;
    }

    /**
     * 设置JWT的Header部分
     *
     * @param header JwtHeader对象，不能为空
     * @throws IllegalArgumentException 当header为空时抛出
     */
    public void setHeader(JwtHeader header) {
        if (null == header) {
            throw new IllegalArgumentException("header is null");
        } else {
            this.header = header;
        }
    }

    /**
     * 获取JWT的Claims部分
     *
     * @return 返回JwtClaims对象
     */
    public JwtClaims getClaims() {
        return this.claims;
    }

    /**
     * 设置JWT的Claims部分
     *
     * @param claims JwtClaims对象，不能为空且必须包含至少一个参数
     * @throws IllegalArgumentException 当claims为空或不包含任何参数时抛出
     */
    public void setClaims(JwtClaims claims) {
        if (null != claims && claims.getParamsSize() != 0) {
            this.claims = claims;
        } else {
            throw new IllegalArgumentException("claims is null");
        }
    }

    /**
     * 获取JWT的HMAC签名部分
     *
     * @return 返回Base64编码的HMAC签名字符串
     */
    public String getHmac() {
        return this.hmac;
    }

    /**
     * 设置JWT的HMAC签名部分
     *
     * @param hmac Base64编码的HMAC签名字符串，不能为空
     * @throws IllegalArgumentException 当hmac为空时抛出
     */
    public void setHmac(String hmac) {
        if (StrUtil.isEmpty(hmac)) {
            throw new IllegalArgumentException("hmac is null");
        } else {
            this.hmac = hmac;
        }
    }

    /**
     * 设置JWT的subject(主题)声明
     *
     * @param subject 主题值，不能为空
     * @throws IllegalArgumentException 当subject为空时抛出
     */
    public void setSubject(String subject) {
        if (StrUtil.isEmpty(subject)) {
            throw new IllegalArgumentException("subject of Claims is null");
        } else {
            this.claims.setSubject(subject);
        }
    }

    /**
     * 设置JWT的issuedAt(签发时间)声明
     *
     * @param date 签发时间，不能为空
     * @throws IllegalArgumentException 当date为空时抛出
     */
    public void setIssuedAt(Date date) {
        if (null == date) {
            throw new IllegalArgumentException("IssuedAt of Claims is null");
        } else {
            this.claims.setIssuedAt(date);
        }
    }

    /**
     * 设置JWT的expiration(过期时间)声明
     *
     * @param date 过期时间，不能为空
     * @throws IllegalArgumentException 当date为空时抛出
     */
    public void setExpiration(Date date) {
        if (null == date) {
            throw new IllegalArgumentException("Expiration of Claims is null");
        } else {
            this.claims.setExpiration(date);
        }
    }


}
